﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Text;
using System.Xml;

using Centido.Core.Dto;

namespace Centido.Core
{
    /// <summary>
    ///   The class that helps to restore a domain backup.
    /// </summary>
    public class RestoreTask : TaskBase
    {
        /// <summary>
        ///   The start string for a table data uri.
        /// </summary>
        public const string TableDataUriStartString = "/TableData/";

        /// <summary>
        ///   The default user password.
        /// </summary>
        public const string DefaultUserPassword = "P@ssw0rd";

        /// <summary>
        ///   The default backup file name.
        /// </summary>
        public const string DefaultFileName = "backup.zip";

        /// <summary>
        ///   The backup file name.
        /// </summary>
        private string _fileName = DefaultFileName;

        /// <summary>
        ///   The dictionary of table column values.
        /// </summary>
        private readonly Dictionary<Guid, ColumnValue> _tableColumnValues
            = new Dictionary<Guid, ColumnValue>();


        /// <summary>
        ///   Initializes a new instance of the <see cref="RestoreTask"/> class.
        /// </summary>
        /// <param name="domain">The target domain.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="domain"/> is null.</exception>
        public RestoreTask(Domain domain)
        {
            if (domain == null)
                throw new ArgumentNullException("domain");

            Domain = domain;
        }


        /// <summary>
        ///   Gets the target domain.
        /// </summary>
        public Domain Domain { get; private set; }


        /// <summary>
        ///   Gets or sets the backup file name.
        /// </summary>
        public string FileName
        {
            get { return _fileName; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                _fileName = value;
            }
        }


        /// <summary>
        ///   Runs the process of backup restoration.
        /// </summary>
        /// <remarks>The current user has to have the permission to change
        /// security settings and the permission to change the schema
        /// in order to perform this operation.</remarks>
        /// <example>
        ///   The following example shows how to make a backup and then restore it.
        /// <code>
        ///     using System;
        ///     using Centido.Core;
        /// 
        ///     class DomainSample
        ///     {
        ///         static void Main()
        ///         {
        ///             var domainConfig = new DomainConfiguration()
        ///             {
        ///                 DbConnectionString = @"Driver={SQL Server};Server=.\SQLEXPRESS;Database=MyDb;Uid=admin;Pwd=P@ssw0rd;",
        ///                 DbProviderName = "MSSQLSERVER"
        ///             };
        ///         
        ///             var domain = Domain.Build(domainConfig);
        ///         
        ///             domain.DeleteAll();
        ///             
        ///             //Creating a table
        ///             var table = domain.AddTable("my_table");
        ///             table.GrantAll();
        /// 
        ///             //Creating a column
        ///             var column = table.AddColumn("my_col", ColumnType.Integer);
        /// 
        ///             // Inserting a row
        ///             var id = table.InsertRow(column.Value(25));
        /// 
        ///             new BackupTask(domain)
        ///               { FileName = "c:\\temp\\backup2.zip" }
        ///               .Run();
        /// 
        ///             domain.DeleteAll();
        /// 
        ///             new RestoreTask(domain)
        ///             { FileName = "c:\\temp\\backup2.zip" }.Run();
        /// 
        ///             // Reading the restored row
        ///             Console.WriteLine("Restored row: my_col = {0}",
        ///               domain.GetTable("my_table").GetColumn("my_col").Read(id).GetInteger());   
        ///         }
        ///     }
        /// </code>
        /// </example>
        public override void Run()
        {
            using (var package = Package.Open(FileName, FileMode.Open))
            {
                // Opens the metadata document
                OpenMetadataDocument(package);

                // Opens all table data documents
                OpenTableDataDocuments(package);
            }
        }


        /// <summary>
        ///   Opens the document with the domain metadata.
        /// </summary>
        /// <param name="package">The current zip package. </param>
        private void OpenMetadataDocument(Package package)
        {
            var metadataDocumentUri = new Uri(BackupTask.MetadataDocumentPath, UriKind.Relative);

            // If the metadata document exists
            if (package.PartExists(metadataDocumentUri))
            {
                var metadataPart = package.GetPart(metadataDocumentUri);

                using (var xmlReader = new XmlTextReader(metadataPart.GetStream()))
                {
                    var backupDto = new BackupDto();

                    backupDto.ReadXml(xmlReader);

                    // Restoring the users
                    RestoreUsers(backupDto);

                    // Restoring the units
                    RestoreUnits(backupDto);

                    // Restoring the roles
                    RestoreRoles(backupDto);

                    // Restoring the schema
                    RestoreSchema(backupDto);
                }
            }
        }


        /// <summary>
        ///   Restoring the user list using the specified data transfer object.
        /// </summary>
        /// <param name="backupDto">The domain data transfer object.</param>
        private void RestoreUsers(BackupDto backupDto)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                "Restoring the users."));

            Domain.DeleteAllUsers();

            // Enumerating all users in the backup
            foreach (var userDto in backupDto.Users
                // ... that are not already added in the domain
                .Where(u => Domain.GetUser(u.Name) == null))
            {
                var user = Domain.AddUser(userDto.Name);

                userDto.CopyTo(user);
            }
        }


        /// <summary>
        ///   Restores the unit list using the specified data transfer object. 
        /// </summary>
        /// <param name="backupDto">The domain data transfer object.</param>
        private void RestoreUnits(BackupDto backupDto)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                "Restoring the units."));

            Domain.DeleteAllUnits();

            // Enumeratins all units in the backup
            foreach (var unitDto in backupDto.Units)
            {
                var unit = Domain.AddUnit(unitDto.Name);

                unitDto.CopyTo(unit);

                // Restoring the unit users
                foreach (var userName in unitDto.UserNames)
                {
                    var user = Domain.GetUser(userName);

                    if (user != null)
                        unit.AddUser(user);
                }
            }

            // Enumerating all units in the backup again
            foreach (var unitDto in backupDto.Units)
            {
                var unit = Domain.GetUnit(unitDto.Name);

                if (unit == null) continue;

                // ... and restoring the unit hierarchy
                foreach (var childUnitName in unitDto.ChildUnitNames)
                {
                    var childUnit = Domain.GetUnit(childUnitName);

                    if (childUnit != null)
                        unit.AddUnit(childUnit);
                }
            }
        }


        /// <summary>
        ///   Restores the role list using the specified data transfer object. 
        /// </summary>
        /// <param name="backupDto">The domain data transfer object.</param>
        private void RestoreRoles(BackupDto backupDto)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                "Restoring the roles."));

            Domain.DeleteAllRoles();

            // Enumerating all roles in the backup
            foreach (var roleDto in backupDto.Roles)
            {
                var role = Domain.AddRole(roleDto.Name);

                roleDto.CopyTo(role);
            }

            // Enumerating all roles in the backup again
            foreach (var roleDto in backupDto.Roles)
            {
                var role = Domain.GetRole(roleDto.Name);

                if (role == null) continue;

                // Restoring the role users
                foreach (var userName in roleDto.UserNames)
                {
                    var user = Domain.GetUser(userName);

                    if (user != null)
                        role.AddUser(user);
                }

                // Restoring the child roles
                foreach (var childRoleName in roleDto.ChildRoleNames)
                {
                    var childRole = Domain.GetRole(childRoleName);

                    if (childRole != null)
                        role.AddRole(childRole);
                }
            }
        }


        /// <summary>
        ///   Restores the target domain schema using the specified data transfer object.
        /// </summary>
        /// <param name="backupDto">The domain data transfer object.</param>
        private void RestoreSchema(BackupDto backupDto)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                "Restoring the schema."));

            Domain.DeleteAllTables();

            // Enumerating all tables in the backup
            foreach (var tableDto in backupDto.Tables)
            {
                // Ensuring that such a table exists
                var table = Domain.GetTable(tableDto.Name) 
                    ?? Domain.AddTable(tableDto.Name);

                tableDto.CopyTo(table);
            }

            // Enumerating all tables in the backup again 
            // (now can we can create references between tables)
            foreach (var tableDto in backupDto.Tables)
            {
                var table = Domain.GetTable(tableDto.Name);

                if (table == null) continue;

                // Restoring the table columns
                RestoreTableColumns(table, tableDto);

                // Removing all table indexes
                foreach (var index in table.Indexes)
                    index.Delete();

                // Recreating all table indexes
                foreach (var indexDto in tableDto.Indexes)
                {
                    if (indexDto.ColumnNames.Count == 0 ) continue;

                    var columnList = indexDto.ColumnNames.Select(table.GetColumn);

                    // Creating a table index
                    table.AddIndex(indexDto.IsUnique ?? false,
                        columnList.ToArray());
                }

                // Recreating table permissions
                foreach (var perm in tableDto.Permissions)
                {
                    // Searching for the principal
                    var principal = Domain.GetPrincipal(perm.PrincipalName);

                    if (principal == null) continue;

                    table.Grant(principal, perm.DataAction, perm.AccessLevel);
                }
            }
        }


        /// <summary>
        ///   Restores table columns from the specified dto.
        /// </summary>
        /// <param name="table">The target table.</param>
        /// <param name="tableDto">The dto for that table.</param>
        private void RestoreTableColumns(Table table, TableDto tableDto)
        {
            // Enumerating all table columns
            foreach (var columnDto in tableDto.Columns.OrderBy(c => c.OrderIndex))
            {
                // Ensuring that such a column exists
                var column = table.GetColumn(columnDto.Name)
                    ?? table.AddColumn(columnDto.Name, columnDto.ColumnType);

                // If the column type is different
                if (column.ColumnType != columnDto.ColumnType)
                {
                    // ... then recreating the column
                    column.Delete();
                    column = table.AddColumn(columnDto.Name, columnDto.ColumnType);
                }

                // If it is the enumeration column
                if (columnDto.ColumnType == ColumnType.Enumeration)
                {
                    var enumColumn = (EnumColumn)column;
                    var enumColumnDto = (EnumColumnDto)columnDto;

                    // Restoring the enumeration members
                    foreach (var enumMemberDto in enumColumnDto.Members)
                    {
                        var enumMember = enumColumn.AddMember(enumMemberDto.Value);

                        enumMemberDto.CopyTo(enumMember);
                    }
                }
                // If it is the reference column
                else if (columnDto.ColumnType == ColumnType.Reference)
                {
                    var refColumn = (RefColumn)column;
                    var refColumnDto = (RefColumnDto)columnDto;

                    // Restoring the reference targets
                    foreach (var refTargetDto in refColumnDto.Targets)
                    {
                        var refTable = Domain.GetTable(refTargetDto.RefTableName);

                        if (refTable != null)
                        {
                            var refTarget = refColumn.AddTarget(refTable);

                            refTargetDto.CopyTo(refTarget);
                        }
                    }
                }

                columnDto.CopyTo(column);
            }
        }


        /// <summary>
        ///   Disables reference checking for all reference columns in the current domain.
        /// </summary>
        /// <returns>Returns a list of those reference columns
        /// that had reference checking before the disabling.</returns>
        private IEnumerable<RefColumn> DisableRefColumnsChecking()
        {
            var resultList = new List<RefColumn>();

            foreach(var table in Domain.Tables)
                foreach(var refColumn in table.RefColumns)
                    if (refColumn.HasReferenceChecking)
                    {
                        refColumn.HasReferenceChecking = false;
                        resultList.Add(refColumn);
                    }

            return resultList;
        }


        /// <summary>
        ///   Reenables reference checking the specified reference columns
        /// </summary>
        /// <param name="refColumns">The reference columns.</param>
        private static void ReenableRefColumnsChecking(IEnumerable<RefColumn> refColumns)
        {
            foreach (var refColumn in refColumns)
                refColumn.HasReferenceChecking = true;
        }


        /// <summary>
        ///   Opens all table data documents in the zip package.
        /// </summary>
        /// <param name="package">The current zip package.</param>
        private void OpenTableDataDocuments(Package package)
        {
            ClearTableColumnValuesDictionary();

            // Disabling reference checking
            var disabledRefColumns = DisableRefColumnsChecking();

            foreach (var part in package.GetParts()
                // Searching for table data
                .Where(p => p.Uri.OriginalString.StartsWith(TableDataUriStartString)))
                OpenTableDataDocument(part);

            // Reenabling reference checking
            ReenableRefColumnsChecking(disabledRefColumns);
        }


        /// <summary>
        ///   Opens a table data document from the specified zip package part.
        /// </summary>
        /// <param name="part">The current zip package part.</param>
        private void OpenTableDataDocument(PackagePart part)
        {
            // Sending a diagnostic message
            OnMessageReceived(new TaskMessageReceivedEventArgs(
                string.Format("Procesing data for '{0}'.", part.Uri.OriginalString)));

            using (var xmlReader = new XmlTextReader(part.GetStream()))
            {
                var tableDataDto = new TableDataDto 
                {
                    ReadHandler = PopulateTableDataRow
                };

                tableDataDto.ReadXml(xmlReader);
            }
        }


        /// <summary>
        ///   Clears the dictionary of table column values.
        /// </summary>
        private void ClearTableColumnValuesDictionary()
        {
            _tableColumnValues.Clear();
        }


        /// <summary>
        ///   Creates a new table column value.
        /// </summary>
        /// <param name="table">The target table.</param>
        /// <param name="columnName">The target column name.</param>
        /// <param name="columnData">The column data.</param>
        /// <returns>Returns a column value for the specified table column.</returns>
        private ColumnValue CreateTableColumnValue(Table table, string columnName, object columnData)
        {
            var column = table.GetColumn(columnName);

            if (!_tableColumnValues.ContainsKey(column.Id))
                _tableColumnValues.Add(column.Id, column.EmptyValue());

            var columnValue = _tableColumnValues[column.Id];
            
            columnValue.Value = columnData;

            return columnValue;
        }


        /// <summary>
        ///   Deserializes the specified column value from a string.
        /// </summary>
        /// <param name="columnValue">The specified column value.</param>
        /// <exception cref="ArgumentNullException">Thrown when the value of <paramref name="columnValue"/> is null.</exception>
        /// <returns>Returns the object representation of the specified column value.</returns>
        private static void DeserializeColumnValue(ColumnValue columnValue)
        {
            if (columnValue == null)
                throw new ArgumentNullException("columnValue");

            var stringValue = (string)columnValue.Value;

            if (stringValue == null) return;

            // If this is a binary column
            if (columnValue.Column is BinaryColumn)
                columnValue.Value = Encoding.Default.GetBytes(stringValue);

            // If this is a datetime column
            if (columnValue.Column is DateTimeColumn)
                columnValue.Value = DateTime.Parse(stringValue, CultureInfo.InvariantCulture);

            // If this is a decimal column
            if (columnValue.Column is DecimalColumn)
                columnValue.Value = Decimal.Parse(stringValue, CultureInfo.InvariantCulture);

            // If this is a boolean column
            if (columnValue.Column is BoolColumn)
                columnValue.Value = Boolean.Parse(stringValue);

            // If this is a reference column
            if (columnValue.Column is RefColumn)
                columnValue.Value = new Guid(stringValue);

            // If this is a guid column
            if (columnValue.Column is GuidColumn)
                columnValue.Value = new Guid(stringValue);

            // If this a enumeration column
            // .. or small integer column
            // .. or integer column
            // .. or big integer column
            if (columnValue.Column is EnumColumn
                || columnValue.Column is SmallIntColumn
                || columnValue.Column is IntColumn
                || columnValue.Column is BigIntColumn)
                columnValue.Value = long.Parse(stringValue, CultureInfo.InvariantCulture);

            // StringColumn and TextColumn have string format
        }


        /// <summary>
        ///   Populates a list of table column values for insertion using the specified table row dto.
        /// </summary>
        /// <param name="table">The target table.</param>
        /// <param name="rowDataDto">The table row dto.</param>
        /// <returns>Returns a list of table column values for the specified table row dto.</returns>
        private IEnumerable<ColumnValue> PopulateTableColumnValuesForInsert(Table table, TableRowDataDto rowDataDto)
        {
            // The column owner
            var owner = !string.IsNullOrEmpty(rowDataDto.Owner) ? rowDataDto.Owner : Domain.CurrentUser.Name;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.Owner,
                DbValue.GetDbValue(owner));

            // The IsActive column
            var isActive = !string.IsNullOrEmpty(rowDataDto.DeletedBy) && rowDataDto.DeletedOn.HasValue ?
                0 : 1;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.IsActive,
                DbValue.GetDbValue(isActive));

            // The DeletedOn column
            var deletedOn = rowDataDto.DeletedOn.HasValue ? rowDataDto.DeletedOn : null;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.DeletedOn,
                DbValue.GetDbValue(deletedOn));

            // The DeletedBy column
            var deletedBy = !string.IsNullOrEmpty(rowDataDto.DeletedBy) ?
                rowDataDto.DeletedBy : null;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.DeletedBy,
                DbValue.GetDbValue(deletedBy));

            // All other columns
            foreach (var columnValueDto in rowDataDto.ColumnValues)
            {
                var columnValue = CreateTableColumnValue(table, columnValueDto.Key, columnValueDto.Value);

                DeserializeColumnValue(columnValue);

                yield return columnValue;
            }
        }


        /// <summary>
        ///   Populates a list of table column values for update using the specified table row dto.
        /// </summary>
        /// <param name="table">The target table.</param>
        /// <param name="rowDataDto">The table row dto.</param>
        /// <returns>Returns a list of table column values for the specified table row dto.</returns>
        private IEnumerable<ColumnValue> PopulateTableColumnValuesForUpdate(Table table, TableRowDataDto rowDataDto)
        {
            // The column version
            var version = rowDataDto.Version != null ? rowDataDto.Version.Value : Guid.NewGuid();

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.RowVersion,
                DbValue.GetDbValue(version));

            // The CreatedOn column
            var createdOn = rowDataDto.CreatedOn.HasValue ? rowDataDto.CreatedOn : DateTime.Now;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.CreatedOn,
                DbValue.GetDbValue(createdOn));

            // The CreatedBy column
            var createdBy = !string.IsNullOrEmpty(rowDataDto.CreatedBy) ?
                rowDataDto.CreatedBy : Domain.CurrentUser.Name;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.CreatedBy,
                DbValue.GetDbValue(createdBy));

            // The UpdatedOn column
            var updatedOn = rowDataDto.UpdatedOn.HasValue ? rowDataDto.UpdatedOn : DateTime.Now;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.UpdatedOn,
                DbValue.GetDbValue(updatedOn));

            // The UpdatedBy column
            var updatedBy = !string.IsNullOrEmpty(rowDataDto.UpdatedBy) ?
                rowDataDto.UpdatedBy : Domain.CurrentUser.Name;

            yield return CreateTableColumnValue(table,
                Db.Tables.UserTable.Col.UpdatedBy,
                DbValue.GetDbValue(updatedBy));
        }


        /// <summary>
        ///   Inserts a row into a tble with the specified name.
        /// </summary>
        /// <param name="tableName">The name of the table.</param>
        /// <param name="rowDataDto">The new table row.</param>
        private void PopulateTableDataRow(string tableName, TableRowDataDto rowDataDto)
        {
            if (tableName == null)
                throw new ArgumentNullException("tableName");

            var table = Domain.GetTable(tableName);

            // If there is no table with the specified name
            if (table == null) return;

            // Inserting a row
            table.InsertRow(rowDataDto.Id, DataSource.InternalTable,
                PopulateTableColumnValuesForInsert(table, rowDataDto).ToArray());

            // Getting a list of column values that should be updated
            var updateColumnList = PopulateTableColumnValuesForUpdate(table, rowDataDto)
                .ToArray();

            if (updateColumnList.Length > 0)
            {
                table.UpdateRow(rowDataDto.Id, DataSource.InternalTable,
                    updateColumnList);
            }
        }
    }
}
